package com.myblockchain.service.impl;

import com.alibaba.fastjson.JSON;
import com.myblockchain.service.BlockService;
import com.myblockchain.service.TransactionService;
import com.myblockchain.service.WalletService;
import com.myblockchain.service.model.*;
import com.myblockchain.utility.CryptoUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * @author Liu
 */
@Service
public class BlockServiceImpl implements BlockService {
    /**
     * 区块链存储
     */
    private List<Block> blockChain = new ArrayList<>();
    /**
     * 当前节点钱包集合
     */
    private Map<String, Wallet> myWalletMap = new HashMap<>();
    /**
     * 其他节点钱包集合，钱包只包含公钥
     */
    private Map<String, Wallet> otherWalletMap = new HashMap<>();
    /**
     * 交易集合
     */
    private List<Transaction> transactionList = new ArrayList<>();

    /**
     * 已经打包进区块的交易集合
     */
    private List<Transaction> packedTransactionList = new ArrayList<>();

    @Autowired
    private  TransactionService transactionService;

    @Autowired
    private WalletService walletService;
    /**
     * 生成创世区块
     */
    public BlockServiceImpl(){
        Block genesisBlock = Block.builder()
                .index(1)
                .timeStamp(System.currentTimeMillis())
                .transactions(new ArrayList<Transaction>())
                .nonce(1)
                .hash("1")
                .previousHash("1")
                .build();
        //new Block("1",1,1,"1",System.currentTimeMillis(),new ArrayList<>());
        blockChain.add(genesisBlock);

    }


    /**
     * 挖矿
     * @param toAddress 矿工钱包地址
     * @return
     */
    @Override
    public Block mine(String toAddress){
//        创建系统奖励的交易
        transactionList.add(newReward(toAddress));
//        去除已经打包进区块的交易
        List<Transaction> rmTransactionList = new ArrayList<>(transactionList);
        rmTransactionList.removeAll(packedTransactionList);
        verifyAllTransaction(rmTransactionList);


//        随机数
        int nonce = 1;
//        hash = 上一个区块的hash + 交易信息 + 随机数
        String newBlockHash = null;
//        挖矿
        while (true){
//            计算过程中的交易集合不会改变
            newBlockHash = calculateHash(getLatestBlock().getHash(), rmTransactionList, nonce);
            if (isValidHash(newBlockHash)){
                System.out.println("mining complete");
                break;
            }
            nonce++;
        }
        Block newBlock = createNewBlock(nonce, getLatestBlock().getHash(), newBlockHash, rmTransactionList);

        return newBlock;
    }


    /**
     * 创建一个新区块
     * @param nonce 随机数
     * @param previousHash 前一个区块的hash
     * @param hash 区块hash
     * @param list 交易集合
     * @return
     */
    private Block createNewBlock(int nonce,
                     String previousHash,
                     String hash,
                     List<Transaction> list) {
        Block block = Block.builder()
                .index(blockChain.size() + 1)
                .timeStamp(System.currentTimeMillis())
                .transactions(list)
                .nonce(nonce)
                .hash(hash)
                .previousHash(previousHash)
                .build();
        //new Block(hash,blockChain.size()+1,nonce,previousHash,System.currentTimeMillis(),list);
        if (addBlock(block)){
            return block;
        }
        return null;

    }

    /**
     * 添加新区块
     * @param block
     * @return
     */
    @Override
    public boolean addBlock(Block block) {
        if (isValidNewBlock(block, getLatestBlock())){
            blockChain.add(block);
            packedTransactionList.addAll(block.getTransactions());
            return true;
        }
        return false;
    }

    /**
     * 检验新区快是否有效
     * @param newBlock
     * @param preBlock
     * @return
     */
    private boolean isValidNewBlock(Block newBlock, Block preBlock) {
        if (!preBlock.getHash().equals(newBlock.getPreviousHash())){
            return false;
        }else {
            String hash = calculateHash(newBlock.getPreviousHash() , newBlock.getTransactions(),newBlock.getNonce());
            if (!hash.equals(newBlock.getHash())){
                return false;
            }
            if (!isValidHash(newBlock.getHash())){
                return false;
            }
        }
        return true;
    }


    /**
     * 检验交易是否有效
     * @param list
     */
    private void verifyAllTransaction(List<Transaction> list) {
        List<Transaction> invalidTxs = new ArrayList<>();
        for (Transaction tr :list){
            if (!verifyTransaction(tr)){
                invalidTxs.add(tr);
            }
        }
        list.removeAll(invalidTxs);
        transactionList.removeAll(invalidTxs);
    }

    /**
     * 生成区块的交易奖励
     * @param toAddress
     * @return
     */
    private Transaction newReward(String toAddress){
        TransactionInput txIn = TransactionInput.builder()
                .value(-1)
                .txId("0")
                .publicKey(null)
                .signature(null)
                .build();
        Wallet wallet = myWalletMap.get(toAddress);
        TransactionOutput txOut = TransactionOutput.builder()
                .value(10)
                .publicHashKey(walletService.hashPubKey(wallet.getPublicKey()))
                .build();
        return Transaction.builder()
                .id(CryptoUtil.UUID())
                .txIn(txIn)
                .txOut(txOut)
                .build();
    }

    /**
     * 单个交易验证
     * @param transaction 需要验证的交易
     * @return
     */
    private boolean verifyTransaction(Transaction transaction) {
        if (transactionService.coinbaseTx(transaction)){
            return true;
        }
        Transaction prevTx = findTransaction(transaction.getTxIn().getTxId());
        return transactionService.verify(transaction, prevTx);
    }

    /**
     * 计算区块的hash值
     * @param preHash
     * @param list
     * @param nonce
     * @return
     */
    private String calculateHash (String preHash, List <Transaction> list, int nonce){
        String hash = CryptoUtil.SHA256(preHash+JSON.toJSONString(list) + nonce);
        return hash;
    }

    /**
     * 验证产生的hash是否合法
     * @param hash
     * @return
     */
    private boolean isValidHash(String hash){
        return hash.startsWith("000");
    }

    /**
     * 获取区块链最后一个区块
     * @return
     */
    @Override
    public Block getLatestBlock() {
        return blockChain.size() > 0 ? blockChain.get(blockChain.size() - 1) : null;
    }


    /**
     * 创建一笔交易
     * @param sender 交易发起方钱包地址
     * @param receiver 交易接收方钱包地址
     * @param amount 交易金额
     * @return
     */
    @Override
    public Transaction createTransaction(Wallet sender, Wallet receiver,int amount){
        List<Transaction> unspentTx = findUnspentTransactions(walletService.getWalletAddress(sender.getPublicKey()));

        Transaction prevTx = null;
        for (Transaction tx : unspentTx){
            // TODO: 2020/2/14  找零
            if (tx.getTxOut().getValue() == amount){
                prevTx = tx;
            }
            break;
        }
        if (prevTx == null){
            return null;
        }
        TransactionInput transactionInput = TransactionInput.builder()
                .txId(prevTx.getId())
                .value(amount)
                .signature(null)
                .publicKey(sender.getPublicKey())
                .build();
        TransactionOutput transactionOutput = TransactionOutput.builder()
                .publicHashKey(walletService.hashPubKey(receiver.getPublicKey()))
                .value(amount)
                .build();
        Transaction transaction = Transaction.builder()
                .txOut(transactionOutput)
                .txIn(transactionInput)
                .id(CryptoUtil.UUID())
                .build();
        transactionService.sign(transaction,sender.getPrivateKey(),prevTx);
        transactionList.add(transaction);
        return transaction;
    }

    /**
     * 查找UTXO
     * @param address 所需要查询的钱包地址
     * @return
     */
    private List<Transaction> findUnspentTransactions(String address) {
        List<Transaction> list = new ArrayList<>();
        Set<String> spentTxs = new HashSet<>();
//        新生成没有被打包的交易
        for (Transaction transaction : transactionList) {
            if (transactionService.coinbaseTx(transaction)){
                continue;
            }
//            记录所有sender花费的交易
            if (walletService.getWalletAddress(transaction.getTxIn().getPublicKey()).equals(address)){
                spentTxs.add(transaction.getTxIn().getTxId());
            }
        }

//        寻找所有sender为右端的交易
        for (Block block : blockChain) {
            List<Transaction> transactions = block.getTransactions();
            for (Transaction transaction : transactions) {
                if(address.equals(CryptoUtil.MD5(transaction.getTxOut().getPublicHashKey()))){
//                    如果UTXO没有被花费，没有被之前的花费引用，则可以使用
                    if (!spentTxs.contains(transaction.getId())){
                        list.add(transaction);
                    }
                }
            }
        }
        return list;
    }

    /**
     * 创建钱包
     */
    @Override
    public Wallet createWallet(){
        Wallet wallet = walletService.generateWallet();
        String address = walletService.getWalletAddress(wallet.getPublicKey());
        myWalletMap.put(address,wallet);
        return wallet;
    }

    @Override
    public Wallet findWallet(String address) {
        return myWalletMap.get(address);
    }

    @Override
    public String findPackedTransactionList() {
        return JSON.toJSONString(packedTransactionList);
    }

    @Override
    public void addOtherWallet(String address, Wallet wallet) {
        otherWalletMap.put(address,wallet);
    }

    @Override
    public void addTransaction(List<Transaction> list) {
        transactionList.addAll(list);
    }

    @Override
    public void addPackedTransaction(List<Transaction> list) {
        packedTransactionList.addAll(list);
    }

    @Override
    public String findAllWallets() {
        List<Wallet> wallets = new ArrayList<>();
        myWalletMap.forEach((address, wallet) ->{
            wallets.add(Wallet.builder().publicKey(wallet.getPublicKey()).build());
        });
        otherWalletMap.forEach((address, wallet) ->{
            wallets.add(wallet);
        });
        return JSON.toJSONString(wallets);
    }

    @Override
    public String findTransactions() {
        return JSON.toJSONString(transactionList);
    }

    /**
     * 查询钱包余额
     * @param address 需要查询的钱包地址
     */
    @Override
    public int getWalletBalance(String address){
        List<Transaction> list = findUnspentTransactions(address);
        int balance = 0;
        for (Transaction transaction : list) {
            balance += transaction.getTxOut().getValue();
        }
        return balance;
    }


    private Transaction findTransaction (String id){
        for (Transaction transaction : transactionList) {
            if (id.equals(transaction.getId())){
                return transaction;
            }
        }
        return null;
    }

    /**
     * 查询区块链信息
     * @return
     */
    @Override
    public String getBlockChain() {
        return JSON.toJSONString(blockChain);
    }
    @Override
    public void replaceChain(List<Block> newBlocks) {
        if (isValidChain(newBlocks) && newBlocks.size() > blockChain.size()){
            blockChain = newBlocks;
            packedTransactionList.clear();
            blockChain.forEach(block -> {
                packedTransactionList.addAll(block.getTransactions());
            });
        }else {
//            System.out.println("received block chain is not valid");
        }
    }

    private boolean isValidChain(List<Block> chain) {
        Block block;
        Block lastBlock = chain.get(0);
        int index = 1;
        while (index < chain.size()){
            block = chain.get(index);
            if (!isValidNewBlock(block,lastBlock)){
                return false;
            }
            lastBlock = block;
            index++;
        }
        return true;
    }
}
